#!/usr/bin/python
# Changelog
# REV 26 - Added log to se for how many people baloo will send the current msg
# REV 27 - Working to add a senders.source and receiver.source
# Out 05 - Fixed the repeat process issue ( see comments below )
#TODO


import string
import sys
import ConfigParser
import socket
import smtplib
import email
import imaplib
import quopri
import re
import calendar
import time
import os
import datetime


# Absolute path
abs_path = os.path.split(os.path.abspath(__file__))[0]

# Function to log actions
def logthis(prefix,msg):
    timestamp = datetime.datetime.ctime(datetime.datetime.now()
    log = "%s - [%s] %s \n" %(timestamp,prefix,msg)
    with open(abs_path+'/baloo.log','a') as f:
        f.write(log)

class Config:
    def __init__(self):
        # Read a conf from absolute path and and store all sections
        self.config = ConfigParser.ConfigParser()
        self.config.read(abs_path+"/baloo.conf")
        self.sections = self.config.sections

    def get_atributes(self,list_name):
        ''' get cfg attributes, must return True, otherwise baloo will exit'''

        # Set name of the list and username as list_name; the list name parameter
        # comes from a section
        self.list_name = list_name
        self.username = list_name

        # This three options must be on list conf for every list, there is no global
        # to use instead
        try:
            self.subject = self.config.get(list_name,'subject')
            self.password = self.config.get(list_name,'password')
        except ConfigParser.NoOptionError,e:
            logthis(self.list_name,e)
            return False

        # This options can't occours together
        try:
            self.source_from = self.config.get(list_name,'source_from') 
            self.source_to = self.config.get(list_name,'source_to')
            self.source = None
        except ConfigParser.NoOptionError,err1:
            try:
                # The same source for sender and receiver
                self.source = self.config.get(list_name,'source')
                self.source_from = None
                self.source_to = None
            except ConfigParser.NoOptionError,err2:
                logthis(self.list_name,err1+err2)
                return False

        # Globals options must exist, even if they don't be used
        if self.check_global() == False: return False

        # Check for a local option, if dosen't exists, try use global
        try:
            self.imapserver = self.config.get(list_name,'imap_host')
        except:
            self.imapserver = self.baloo_imapserver
        try:
            self.smtpserver = self.config.get(list_name,'smtp_host')
        except:
            self.smtpserver = self.baloo_smtpserver

        #TODO Here still can fail, must set a default domain and max_size
        try:
            self.domain = self.config.get(list_name,'domain')
        except:
            self.domain = self.config.get("main",'domain')
        try:
            self.max_size = int(self.config.get(list_name,'max_size'))
        except:
            self.max_size = int(self.config.get("main",'max_size'))
        return  True

    def get_source(self):
        if self.source == None:
            self.SENDERS = []
            self.RECEIVERS = []
            try:
                for source in self.source_from.split(','):
                    with open(abs_path+"/sources/"+source) as f:
                        self.SENDERS += f.readlines()
                    # Remove newline char
                    self.SENDERS = map(string.strip,self.SENDERS)

                for source in self.source_to.split(','):
                    with open(abs_path+"/sources/"+source) as f:
                        self.RECEIVERS += f.readlines()
                    # Remove newline char
                    self.RECEIVERS = map(string.strip,self.RECEIVERS) 
                return True
            except IOError,e:
                logthis(self.list_name,e)
                return False
        else:
            try:
                USER_DATABASE = []
                for source in self.source.split(','):
                    with open(abs_path+"/sources/"+source) as f:
                        USER_DATABASE += f.readlines()
                    # remove newline char
                    USER_DATABASE = map(string.strip,USER_DATABASE)
                    self.SENDERS = USER_DATABASE
                    self.RECEIVERS = USER_DATABASE
                return True
            except IOError,e:
                logthis(self.list_name,e)
                return False

    def check_global(self):
        try:
            self.baloo_imapserver = self.config.get("main",'imap_host')
            self.baloo_smtpserver = self.config.get("main",'smtp_host')
            self.baloo_username = self.config.get("main",'username')
            self.baloo_password = self.config.get("main",'password')
            return True
        except ConfigParser.NoOptionError,e:
            logthis(self.list_name,e)
            return False

class Baloo:

    def __init__(self,list_name):
        self.cfg = Config()
        if self.cfg.get_atributes(list_name) == True:
            if self.cfg.get_source() == True:
                self._connect()
                print list_name
                print " - Qtos podem enviar",len(self.cfg.SENDERS)
                print " - Qtos recebem",len(self.cfg.RECEIVERS)


    def _connect(self):
        try:
            # Log-in in imap using SSL
            self.conn = imaplib.IMAP4_SSL(self.cfg.imapserver)
            self.conn.login(self.cfg.username,self.cfg.password)

            if self.conn.select('VALID')[0] == 'NO': self.conn.create('VALID')
            if self.conn.select('OVERSIZE')[0] == 'NO': self.conn.create('OVERSIZE')
            if self.conn.select('INVALID')[0] == 'NO': self.conn.create('INVALID')
            if self.conn.select('CMD')[0] == 'NO': self.conn.create('CMD')

            # Select inbox, if success go ahead and retrieve messages
            if self.conn.select('INBOX')[0] != 'NO':
                return_code, data_range = self.conn.search(None,'ALL')
                self._retrieve(data_range)
            else:
                logthis(self.cfg.list_name,"inbox not found")

        # Handle exceptions when can't connect to imap server
        except imaplib.IMAP4.error,e:
            logthis(self.cfg.list_name,e)
        except socket.gaierror,e:
            logthis(self.cfg.list_name,e)

    # (1) For every message id, create a MailMessage Object with msg_id, and fully
    # retrieved message (2) check the message to see if it is valid or not (3) Remove
    # message from imbox (4) disconnect
    def _retrieve(self,data_range):
        if data_range != ['']:
            logthis(self.cfg.list_name,"Processing %d messages" % (len(data_range[0].split())))
        for msg_id in data_range[0].split():
            self.mm = MailMessage(msg_id,self.conn.fetch(msg_id,'(RFC822)'),self.cfg)
            CheckMessage(self.mm,self.cfg,self.conn)
        self.conn.expunge() 
        self.conn.logout()

class CheckMessage:

    def __init__(self,message,config,connection):
        self.mm = message
        self.cfg = config
        self.conn = connection
        self.apply_rules()

    def apply_rules(self):
        # Authorization based on True/False, for every rule add True/False flag
        # and check at the end of process
        send_flag = True
        forward_flag = [] 
        forward_flag.append(self.is_allowed_user(self.mm.return_path))
        forward_flag.append(self.check_size())
        forward_flag.append(self.check_headers())

        if forward_flag[0] != True:
            logthis(self.cfg.list_name, "Do not send message from " + self.mm.mail_from)
            self.conn.copy(self.mm.id,'INVALID')
            self.conn.store(self.mm.id, '+FLAGS', '\\Deleted') 
            send_flag = False

        if forward_flag[1] != True:
            logthis(self.cfg.list_name, "Return a message, complaining about max_size")
            self.conn.copy(self.mm.id,'OVERSIZE')
            self.conn.store(self.mm.id, '+FLAGS', '\\Deleted')
            send_flag = False

        if forward_flag[2] != True:
            logthis(self.cfg.list_name, "Mail message with incomplete headers")
            self.conn.copy(self.mm.id,'INVALID')
            self.conn.store(self.mm.id, '+FLAGS', '\\Deleted') 
            send_flag = False

        if send_flag == True:
            logthis(self.cfg.list_name, "* Sending message from " + self.mm.mail_from )
            self.conn.copy(self.mm.id,'VALID')
            self.conn.store(self.mm.id, '+FLAGS', '\\Deleted') 
            # If the expunge is done after sending all messages, could cause a bug,
            # baloo will run multiple times when big messages are sent to a large
            # ammount of people. ( Occurs when crontab time interval is low)
            # So, just keep it here!
            self.conn.expunge() 
            # Been the last thing to be done, becomes a fail-safe to avoid loops
            self._send() 

    def check_headers(self):
        if self.mm.return_path == None: return False
        if self.mm.mail_from == None: return False
        if self.mm.mail_to == None: return False
        if self.mm.mail_subject == None: return False
        if self.mm.received == None: return False
        return True

    def check_size(self):
        maxsize = self.cfg.max_size + 1000000
        print "Maxsize is",maxsize
        if int(self.mm.size) > maxsize:
            return False
        else:
            return True

    def is_allowed_user(self,return_path):
        for user in self.cfg.SENDERS:
            user = user.replace('\n','')
            user = user.replace(' ','')
            if user != '':
                if return_path == '':
                    return False
                if user == return_path:
                    string = "User match(return_path):%s,%s" %(user,return_path)
                    logthis(self.cfg.list_name,string)
                    return True
                if re.search(user,self.mm.mail_from) !=None:
                    string = "User match(mail_from):%s,%s" %(user,self.mm.mail_from)
                    logthis(self.cfg.list_name,string)
                    return True

        # Check if user is a joker
        # need to cleanup this part
        if os.path.isfile('joker.senders'):
            with open('joker.senders','r') as f:
                jokers = f.readlines()

            for user in jokers:
                user = user.replace('\n','')
                user = user.replace(' ','')
                if user != '':
                    if return_path == '':
                        return False
                    if user == return_path:
                        string = "User match as Joker(return_path):%s,%s" %(user,return_path)
                        logthis(self.cfg.list_name,string)
                        return True
                    if re.search(user,self.mm.mail_from) !=None:
                        string = "User match as Joker(mail_from):%s,%s" %(user,self.mm.mail_from)
                        logthis(self.cfg.list_name,string)
                        return True

        return False

    def _send(self):
        server = smtplib.SMTP(self.cfg.smtpserver,25)
        server.ehlo()
        server.starttls()
        server.ehlo()
        server.login(self.cfg.username,self.cfg.password)

        # Copy message object to a local variable
        msg = self.mm.msg
        # Add a FROM_header ( Original sender )
        msg.add_header('From',self.mm.mail_from)
        # Add a TO_header ( Users subscribed to this list )
        msg.add_header('To',self.mm.mail_to)
        # Change SUBJECT_header ( title defined in baloo.conf)
        msg.replace_header('Subject',self.cfg.subject+" "+self.mm.mail_subject) 


        logthis(self.cfg.list_name," - sending to " + str(len(self.cfg.RECEIVERS)))
        for user in self.cfg.RECEIVERS:
            user = user.replace('\n','')
            if user != '':
                try:
                    server.sendmail(self.cfg.username,user,msg.as_string())
                except:
                    pass
        server.quit()

class MailMessage:

    def __init__(self, msg_id, result, config):
        self.cfg = config
        self.id = msg_id
        rt_cod, mail_data = result

        self.msg = self.create_new_message_object(mail_data)
        self.size = self.get_size(mail_data)

        #Actions
        self.get_header_info()
        self.remove_original_headers()
        self.add_new_headers()

    def create_new_message_object(self,mail_data):
        '''
        Receive a message in valid mail text format and convert to a mail message
        python object.

        Return this object
        '''
        return email.message_from_string(mail_data[0][1])

    def get_header_info(self):
        '''
        From mail message object, try to grab a few headers
        '''
        self.return_path = self.msg.get('return-path')[1:-1]
        self.mail_from = self.msg.get('from')
        self.mail_to = self.msg.get('to')
        self.mail_subject = self.msg.get('subject')
        self.received = self.msg.get('received')

    def get_size(self,mail_data):
        '''
        Return the size of the message
        '''
        return re.findall('{[0-9]*}',mail_data[0][0])[0][1:-1]


    def remove_original_headers(self):
        '''
        Try to remove a serie of headers from original message
        '''
        for item in ['return-path','from','received','to','X-Original-to','delivered-to','message-id','date','User-agent']:
            del(self.msg[item])

        for item in ['Return-path','From','Received','To','X-Original-to','delivered-to','message-id','date','User-agent']:
            del(self.msg[item])

    def add_new_headers(self):
        '''
        This function should add custom fixed headers, such as List Headers
        '''
        list_id = '<%s.%s>' % (self.cfg.list_name,self.cfg.domain) 
        list_archieve = 'not avaiable yet'
        list_help = '<mailto:baloo@%s?subject=help>' % self.cfg.domain
        list_owner = '<mailto:baloo@%s' % self.cfg.domain
        list_post ='<mailto:%s@%s>' % (self.cfg.list_name,self.cfg.domain)
        list_subscribe ='<mailto:%s@%s?subject=subscribe>' % (self.cfg.list_name,self.cfg.domain)
        list_unsubscribe = '<mailto:%s@%s?subject=unsubscribe>' % (self.cfg.list_name,self.cfg.domain)
        self. msg.add_header('List-id',list_id)
        self.msg.add_header('List-Archive',list_archieve)
        self.msg.add_header('List-Help',list_help)
        self.msg.add_header('List-Owner',list_owner)
        self.msg.add_header('List-Post',list_post)
        self.msg.add_header('List-Subscribe',list_subscribe)
        self.msg.add_header('List-Unsubscribe',list_unsubscribe)


if __name__ == '__main__':
    config = Config()
    sections = config.sections()
    
    for section in sections:
        if section != "main":
            Baloo(section)

